//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace Microsoft.Research.eResearch.Execution
{
    using System;
    using System.Collections.Generic;
    using System.Configuration;
    using System.Globalization;
    using System.IO;
    using System.Threading;
    using Microsoft.Research.ScientificWorkflow.TridentComposer;
    using Microsoft.Practices.EnterpriseLibrary.ExceptionHandling;
    using Microsoft.Research.DataLayer;
    using Microsoft.Research.eResearch.Common;
    using Microsoft.Research.eResearch.Common.Validation;
    using TridentAPI;

    /// <summary>
    /// Creates the sandbox environment and downloads the required dlls from registry.
    /// </summary>
    internal class Sandbox : JobBase
    {
        /// <summary>
        /// Target folder where the shadow copy of the executor DLLs are present.
        /// </summary>
        private string targetFolder;

        /// <summary>
        /// Library folder where the downloaded DLLs are present.
        /// </summary>
        private string libFolder;

        /// <summary>
        /// Max re-try attempts to copy files from the cache.
        /// </summary>
        private int maxRetryCount = 1;

        /// <summary>
        /// Cache Folder where all the assembly images are cached.
        /// </summary>
        private string cacheFolderPath;

        /// <summary>
        /// Flag indicating whether the cache needs to be by-passed.
        /// </summary>
        private bool doByPassCache;

        /// <summary>
        /// Initializes a new instance of the Sandbox class.
        /// </summary>
        /// <param name="jobId">Id of current Job.</param>
        /// <param name="targetFolder">Sanbox folder.</param>
        /// <param name="libFolder">Lib subfolder in the sandbox.</param>
        private Sandbox(Guid jobId, string targetFolder, string libFolder, bool isLightWeightExecution)
            : base(jobId, isLightWeightExecution)
        {
            this.targetFolder = targetFolder;
            this.libFolder = libFolder;

            string configuredCachePath = ConfigurationManager.AppSettings["CachePath"];
            if (!string.IsNullOrEmpty(configuredCachePath))
            {
                this.cacheFolderPath = configuredCachePath.Replace("%appdata%", Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData));
            }

            string configuredRetryCount = ConfigurationManager.AppSettings["FileCopyRetryCount"];
            if (!string.IsNullOrEmpty(configuredRetryCount))
            {
                this.maxRetryCount = Int32.Parse(configuredRetryCount, CultureInfo.InvariantCulture);
            }

            string configuredByPassCacheFlag = ConfigurationManager.AppSettings["ByPassCache"];
            if (!string.IsNullOrEmpty(configuredByPassCacheFlag))
            {
                this.doByPassCache = Boolean.Parse(configuredByPassCacheFlag);
            }
        }

        /// <summary>
        /// Gets the path of the XOML file.
        /// </summary>
        private string XomlPath
        {
            get { return Path.Combine(this.libFolder, this.RootActivity.XomlFileName); }
        }

        /// <summary>
        /// Gets the root activity of the workflow.
        /// </summary>
        private Activity RootActivity
        {
            get { return this.WorkflowJob.ActivityInstances[0].Activity; }
        }

        /// <summary>
        /// Creates the sanbox environment and downloads the necessary dlls.
        /// </summary>
        /// <param name="jobId">Id of current job.</param>
        /// <param name="targetFolder">Sanbox folder.</param>
        /// <param name="libFolder">Lib subfolder in the sandbox.</param>
        /// <returns>Instance of the sandbox.</returns>
        public static Sandbox Create(Guid jobId, string targetFolder, string libFolder, bool isLightWeightExecution)
        {
            // Validate arguments.
            CustomValidator.AssertNotNull(jobId, "jobId");
            CustomValidator.AssertNotEmpty(targetFolder, "targetFolder");
            CustomValidator.AssertNotEmpty(libFolder, "libFolder");
            CustomValidator.AssertRule<string>(path => !Directory.Exists(path), targetFolder, Messages.PathNotExists(targetFolder));
            CustomValidator.AssertRule<string>(path => !Directory.Exists(path), libFolder, Messages.PathNotExists(libFolder));

            // Create a new instance of the sandbox.
            Sandbox sandbox = null;

            ExceptionHandler.Handle(
            PolicyName.LoggingRethrowPolicy,
            () =>
            {
                // Create a new instance of the sandbox.
                sandbox = new Sandbox(jobId, targetFolder, libFolder, isLightWeightExecution);

                // Download workflow XOML.
                sandbox.DownloadWorkflowXoml();

                // Download all the dll images.
                sandbox.DownloadLibraries();

                // Download Dll which are related to blackbox activities.
                if (sandbox.RootActivity.IsBlackbox)
                {
                    sandbox.DownloadBlackBoxAssemblyImages(sandbox.RootActivity);
                }
            },
            () =>
            {
                // Close the connection.
                sandbox.CloseRegistryConnection();
            });

            return sandbox;
        }

        /// <summary>
        /// Downloads the file from the registry.
        /// </summary>
        /// <param name="image">Instance of Assembly image.</param>
        /// <param name="libFilePath">Folder path where the file needs to be downloaded.</param>
        private static void DownloadFromRegistry(AssemblyImage image, string libFilePath)
        {
            if (image.Contents != null)
            {
                File.WriteAllBytes(libFilePath, image.Contents);
            }
        }

        /// <summary>
        /// Downloads the file to the cache folder from the registry.
        /// </summary>
        /// <param name="image">Instance of the Assembly image.</param>
        /// <param name="cacheFilePath">Cache folder location.</param>
        /// <returns>Flag indicating whether the file was successfully copied.</returns>
        private static bool DownloadToCache(AssemblyImage image, string cacheFilePath)
        {
            bool fileDownloaded = false;

            ExceptionHandler.Handle(
                PolicyName.LoggingPolicy,
                () =>
                {
                    if (image.Contents != null)
                    {
                        File.WriteAllBytes(cacheFilePath, image.Contents);
                        fileDownloaded = true;
                    }
                });

            return fileDownloaded;
        }

        /// <summary>
        /// Download workflow XOML from registry.
        /// </summary>
        private void DownloadWorkflowXoml()
        {
            File.WriteAllText(XomlPath, RootActivity.XomlContents);
        }

        /// <summary>
        /// Downloads all libraries from the basesequence.
        /// </summary>
        private void DownloadLibraries()
        {
            Queue<ActivitySequence> pendingActivitySequences = new Queue<ActivitySequence>();
            pendingActivitySequences.Enqueue(RootActivity.BaseSequence);
            Connection conn = this.RegistryConnection.Clone();
            conn.Open();

            while (pendingActivitySequences.Count != 0)
            {
                ActivitySequence sequence = pendingActivitySequences.Dequeue();

                // Download assembly images of the activity
                DownloadAssemblyImages(sequence);
                TypeProviderHelper.Initialize(conn);
                ExternalRegistryHelperProxy.Initialize(conn);

                ActivityComposer.DownloadCustomInitializerLibraries(sequence, targetFolder);

                // Enqueue all child activities.
                foreach (ActivitySequence childSequence in sequence.Children)
                {
                    pendingActivitySequences.Enqueue(childSequence);
                }
            }
        }

        /// <summary>
        /// Donwloads assembly images of the activity.
        /// </summary>
        /// <param name="sequence">Instance of the activity sequence.</param>
        private void DownloadAssemblyImages(ActivitySequence sequence)
        {
            // Sort based on name
            sequence.Activity.AssemblyPackages.GetEnumerator();
            sequence.Activity.AssemblyPackages.Sort((p1, p2) => DateTime.Compare(p2.Updated.When, p1.Updated.When));

            // foreach (AssemblyPackage package in sequence.Activity.AssemblyPackages)
            if (sequence.Activity.AssemblyPackages.Count > 0)
            {
                AssemblyPackage package = sequence.Activity.AssemblyPackages[0];

                // Download the libraries form the activity.
                foreach (AssemblyImage img in package.AssemblyImages)
                {
                    DownloadAssemblyImage(img);
                }
            }
        }

        /// <summary>
        /// Downloads assembly images of the black box workflow.
        /// </summary>
        /// <param name="activity">Instance of the activity sequence.</param>
        private void DownloadBlackBoxAssemblyImages(Activity activity)
        {
            activity.AssemblyPackages.GetEnumerator();

            foreach (AssemblyPackage package in activity.AssemblyPackages)
            {
                // Download the libraries form the activity.
                foreach (AssemblyImage img in package.AssemblyImages)
                {
                    DownloadAssemblyImage(img);
                }
            }
        }

        /// <summary>
        /// Downloads the required assembly image to the job folder. 
        /// The assembly images are fetched from the cached location unless
        /// there is an exception in which case it is fetched from the registry.
        /// In the case where the file is not present in the cache it is first 
        /// downloaded to the cache from the registry and then it is copied to the
        /// job folder.
        /// </summary>
        /// <param name="image">Instance of the assembly image.</param>
        private void DownloadAssemblyImage(AssemblyImage image)
        {
            ExceptionHandler.Handle(
                PolicyName.LoggingRethrowPolicy,
                () =>
                {
                    string targetFile = Path.Combine(targetFolder, image.Filename);
                    string libFile = Path.Combine(libFolder, image.Filename);

                    if (!File.Exists(targetFile) && !File.Exists(libFile))
                    {
                        if (!doByPassCache)
                        {
                            string cacheFileName = ((IObject)image).ID.ToString() + ".dat";
                            string cacheFilePath = Path.Combine(cacheFolderPath, cacheFileName);

                            // create the cache folder if it does not exist
                            if (!Directory.Exists(cacheFolderPath))
                            {
                                ExceptionHandler.Handle(
                                    PolicyName.LoggingPolicy,
                                    () =>
                                    {
                                        Directory.CreateDirectory(cacheFolderPath);
                                    });
                            }

                            // Download to the cache folder if the file is not present there.Then
                            // copy it to the job folder
                            if (!File.Exists(cacheFilePath))
                            {
                                if (!DownloadToCache(image, cacheFilePath))
                                {
                                    DownloadFromRegistry(image, libFile);
                                    return;
                                }
                            }

                            if (!FetchFromCache(cacheFilePath, libFile))
                            {
                                DownloadFromRegistry(image, libFile);
                            }
                        }
                        else
                        {
                            DownloadFromRegistry(image, libFile);
                        }
                    }
                });
        }

        /// <summary>
        /// Fetches the file from the cache location. A configured number of re-tries are
        /// allowed. If the max re-tries are exhausted the file will be downloaded from the
        /// registry.
        /// </summary>
        /// <param name="cacheFilePath">Cache folder location.</param>
        /// <param name="libFilePath">Job folder path.</param>
        /// <returns>Flag indicating whether the file was successfully fetched from the cache.</returns>
        private bool FetchFromCache(string cacheFilePath, string libFilePath)
        {
            int retryCount = 0;

            while (true)
            {
                try
                {
                    File.Copy(cacheFilePath, libFilePath, true);
                    return true;
                }
                catch (Exception ex)
                {
                    if (retryCount >= maxRetryCount)
                    {
                        ExceptionPolicy.HandleException(ex, PolicyName.LoggingPolicy.ToString());
                        break;
                    }
                    else
                    {
                        retryCount++;
                        Thread.Sleep(1000);
                    }
                }
            }

            return false;
        }
    }
}

